#!/usr/bin/perl
use warnings ;
use strict ;

use IO::Socket::INET ;
use IO::Interface::Simple ;
use IO::Select ;

use Net::Address::IP::Local ;
use LWP::UserAgent ;
use JSON ;
use JSON::Parse 'valid_json';
use threads ;
use threads::shared ;
use Thread::Suspend ;
use Term::ReadKey ;

use Getopt::Long ;

use Carp::Assert ;
use Data::Dumper ;

use Time::HiRes qw( usleep ualarm gettimeofday tv_interval nanosleep
                  clock_gettime clock_getres clock_nanosleep clock
                  stat );

use Config;
$Config{useithreads} or die('Recompile Perl with threads to run this program.');

my $Debug = '';
my $VE_Dev_NA = 1 ;

my $OptionRetVal = GetOptions (	'debug' => \$Debug );

my $VE_Dev_Name_By_IP_HashRef ;
my $IP_Corresponding_To_Dev_Name_HashRef ;
my $Chosen_VE_Dev ;
my $JSON = JSON->new->allow_nonref ;
my $UA = LWP::UserAgent->new();
$UA->timeout(1);

my @LogPower :shared = (0, 0, 0, 0, 0) ;
my @SocketsUpdatedName :shared ; 
my $IPStr :shared ;
my $WriteToCSVFile :shared = 0 ;

my @SocketsUpdatedState ;
my @PowerThreads ;

$PowerThreads[0] = threads->create (\&MeasurePower, 0);
$PowerThreads[1] = threads->create (\&MeasurePower, 1);
$PowerThreads[2] = threads->create (\&MeasurePower, 2);
$PowerThreads[3] = threads->create (\&MeasurePower, 3);
$PowerThreads[4] = threads->create (\&MeasurePower, 4);

my $OnlyOneVEDev = 0 ;
my $Chosen_VE_Dev_With_IP = "N/A" ;
my $ChosenIP ;
my @VE_Dev_List ;
my @IP_List ;

&Refresh_VE_Devices ;

if (scalar @VE_Dev_List == 1) {

	$OnlyOneVEDev = 1 ;
	$Chosen_VE_Dev_With_IP = $VE_Dev_List[0] ;
	$ChosenIP = $IP_List[0] ;
	$IPStr = "http://" . $ChosenIP . "/" ;

}	

while (1) {

	&ClearScreen ();
	print "\nVisible Energy Command Center\n" ;
	print "\nCurrent Active Visible Energy Device : $Chosen_VE_Dev_With_IP\n" ;
	print "\nMenu:\n" ;
	print "\n1. Rescan Network / Choose Visible Energy Device" ;
	print "\n2. Check / Alter Socket Status / Name for Chosen Device" ;
	print "\n3. Power Logging Options for Chosen Device" ;
	print "\n4. Exit Command Center" ;
	print "\n\nEnter choice: ";

	my $Option = <STDIN>;
	chomp $Option ;
	
	assert ( ($Option >= 1 && $Option <= 4), 'Selected Option Unavailable' ) ;
	assert ( ($Option == 1 || $Option == 4 || $VE_Dev_NA == 0), 'Selected Option Unavailable - No Visible Energy Device' );

	if ($Option == 1) {
		
		&Refresh_VE_Devices ;
		print "\nAvailable Visible Energy Devices:\n" ;
		for (my $Ctr = 0; $Ctr < scalar @IP_List ; $Ctr ++) {
			printf "\n%d: %s", $Ctr + 1, $VE_Dev_List[$Ctr] ;
		}
		print "\n\nChoose Device to Control: ";
		my $DevToControl = <STDIN> ;
		chomp $DevToControl ;

		assert ( ($DevToControl >= 1 && $DevToControl <= scalar @IP_List) , 'Selected Device Not Available' );

		$Chosen_VE_Dev_With_IP = $VE_Dev_List[$DevToControl - 1];
		$ChosenIP = $IP_List[$DevToControl - 1];
		$IPStr = "http://" . $ChosenIP . "/" ;

	} elsif ($Option == 2) {
	
		my $ShowSocketMenu = 1 ;

		while ($ShowSocketMenu == 1) {

			my $CurrPage = $IPStr . "status.js" ;
			my $GetResponse0 ;
			my $DevStatusJSON ;
			do {
				$GetResponse0 = $UA->get ($CurrPage) ;
			} while !($GetResponse0->is_success) ;
			$DevStatusJSON = $GetResponse0->decoded_content ;
			my $JSONText = $JSON->decode ($DevStatusJSON) ;
			my $SocketInfoArrayRef = $JSONText->{strip}->{status}->{socket} ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				$SocketsUpdatedName[$Ctr]  = $SocketInfoArrayRef->[$Ctr]->{name} ;
				$SocketsUpdatedState[$Ctr] = $SocketInfoArrayRef->[$Ctr]->{state} ;
			}		
			&ClearScreen ;
			print "\nVisible Energy Command Center\n" ;
			print "\nSocket Status & Control for $Chosen_VE_Dev_With_IP\n" ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				printf "\n%d: %s : %s", $Ctr+1, $SocketsUpdatedName[$Ctr], $SocketsUpdatedState[$Ctr] ;
			}
			print "\n\nMenu:\n" ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				printf "\n%d. Toggle Socket %d Status, i.e, Turn %s %s",
					$Ctr + 1, $Ctr + 1, $SocketsUpdatedName[$Ctr], 
					($SocketsUpdatedState[$Ctr] eq "ON") ? "OFF" : "ON" ;
			}
			for (my $Ctr = 4; $Ctr < 8; $Ctr ++) {
				printf "\n%d. Change Socket %d [ %s ] Name",
					$Ctr + 1, $Ctr - 3, $SocketsUpdatedName[$Ctr - 4] ;
			}
			printf "\n9. Return to Main Menu" ;
			print "\n\nEnter choice: ";
			my $SocketMenuOption = <STDIN>;
			chomp $SocketMenuOption ;

			assert ( ($SocketMenuOption >= 1 && $SocketMenuOption <= 9), 'Selected Option Not Available' );

			if ($SocketMenuOption == 9) {
				$ShowSocketMenu = 0 ;
			} else {
				if ($SocketMenuOption >= 1 && $SocketMenuOption <= 4) {
					my $TmpPage = sprintf "%s%d/set.xml?value=%s", $IPStr, $SocketMenuOption - 1,
						($SocketsUpdatedState[$SocketMenuOption - 1] eq "ON") ? "off" : "on" ;
					do {
						$GetResponse0 = $UA->get ($TmpPage) ;
					} while !($GetResponse0->is_success);
				} else {
					printf "\nChange %s to : ", $SocketsUpdatedName[$SocketMenuOption - 5] ;
					my $NewSocketName = <STDIN> ;
					chomp $NewSocketName ;
					my $TmpPage = sprintf "%s%d/config/name.xml?name=%s", $IPStr, $SocketMenuOption - 5,$NewSocketName ;
					do {
						$GetResponse0 = $UA->get ($TmpPage);
					} while !($GetResponse0->is_success);
				}
			}

		}

	} elsif ($Option == 3) {

		my $ShowSocketMenu = 1 ;

		while ($ShowSocketMenu == 1) {

			my $CurrPage = $IPStr . "status.js" ;
			my $GetResponse1 ;
			my $DevStatusJSON ;
			do {
				$GetResponse1 = $UA->get ($CurrPage) ;
			} while !($GetResponse1->is_success);
			$DevStatusJSON = $GetResponse1->decoded_content ;
			my $JSONText = $JSON->decode ($DevStatusJSON) ;
			my $SocketInfoArrayRef = $JSONText->{strip}->{status}->{socket} ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				$SocketsUpdatedName[$Ctr]  = $SocketInfoArrayRef->[$Ctr]->{name} ;
				$SocketsUpdatedState[$Ctr] = $SocketInfoArrayRef->[$Ctr]->{state} ;
			}		
			&ClearScreen ;
			print "\nVisible Energy Command Center\n" ;
			print "\nPower Logging Options for $Chosen_VE_Dev_With_IP\n" ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				printf "\n%d: %s : %s", $Ctr+1, $SocketsUpdatedName[$Ctr], $SocketsUpdatedState[$Ctr] ;
			}
			print "\n\nMenu:\n" ;
			for (my $Ctr = 0; $Ctr < 4; $Ctr ++) {
				printf "\n%d. Power Logging for Socket %d [ %s ] (%s)",
					$Ctr + 1, $Ctr + 1, $SocketsUpdatedName[$Ctr], 
					($SocketsUpdatedState[$Ctr] eq "ON") ? "Available" : "Not Available" ;
			}
			printf "\n5. Power Logging for All Sockets Simultaneously" ;
			printf "\n6. Return to Main Menu" ;
			print "\n\nEnter choice: ";
			my $SocketMenuOption = <STDIN>;
			chomp $SocketMenuOption ;

			assert ( ($SocketMenuOption >= 1 && $SocketMenuOption <= 6), 'Selected Option Not Available' );

			if ($SocketMenuOption < 5) {
				assert ( ($SocketsUpdatedState[$SocketMenuOption - 1] eq "ON"), 'Power Logging Not Available for Switched Off Socket' );
			}

			if ($SocketMenuOption == 6) {
				$ShowSocketMenu = 0 ;
			} else {
				print "\nPress (P - Start Logging to CSV File, L - Start Logging to Screen, Q - Stop Logging)\n" ;
				my $WaitForInputs = 1 ;
				while ($WaitForInputs == 1) {
					ReadMode 4 ;
					my $KeyPress ;
					while (not defined ($KeyPress = ReadKey(-1))) {
					} 
					if ($KeyPress eq "P" || $KeyPress eq "p") {
						$LogPower[$SocketMenuOption - 1] = 1 ;
						$WriteToCSVFile = 1 ;
						if ($PowerThreads[$SocketMenuOption - 1]->is_suspended() != 0) {
							$PowerThreads[$SocketMenuOption - 1]->resume();
						}
					} elsif ($KeyPress eq "L" || $KeyPress eq "l") {
						$LogPower[$SocketMenuOption - 1] = 1 ;
						$WriteToCSVFile = 0 ;
						if ($PowerThreads[$SocketMenuOption - 1]->is_suspended() != 0) {
							$PowerThreads[$SocketMenuOption - 1]->resume();
						}
					} elsif ($KeyPress eq "Q" || $KeyPress eq "q") {
						$LogPower[$SocketMenuOption - 1] = 0 ;
						$WaitForInputs = 0;
					}
				}
				ReadMode 0 ;
			}

		}

	} else {

		exit(0);

	}

}

sub Refresh_VE_Devices {

	my @VE_Device_IPs ;
	
	my @Interfaces = IO::Interface::Simple->interfaces ;
	my @LocalIPList ;

	for my $if (@Interfaces) {
		if (($if->is_running) && !($if->address eq '127.0.0.1')) {
			push (@LocalIPList, $if->address) ;
		}
	}

	foreach my $LocalIP (@LocalIPList) {

		printf "\nAssuming local IP = %s", $LocalIP ;

		my $Socket = IO::Socket::INET-> new (
			LocalAddr => $LocalIP,
			LocalPort => '61735',
			PeerAddr => '255.255.255.255',
			PeerPort => '48005',
			Proto => 'udp',
			Broadcast => 1
			) || die "[ERROR] $!\n" ;

		$Socket->sockopt (SO_BROADCAST, 1);
		print $Socket "RDEV\0\0\0\0" ;
	
		$Socket->close();
	
		my $Listener = IO::Socket::INET-> new (
			LocalPort => '61735',
			Proto => 'udp'
			) || die "Can't create UDP listener : $!\n" ;
	
		my $RecdData ;
	
		my @Ready = IO::Select->new($Listener)->can_read(4) ;
	
		while (scalar @Ready != 0) {
	
			$Listener->recv ($RecdData, 1024);
	
			if ($RecdData =~ m/IDEV/) {
	
				my $VE_Dev_Addr = $Listener->peerhost ;
				push (@VE_Device_IPs, $VE_Dev_Addr) ;
	
			}
	
			@Ready = IO::Select->new($Listener)->can_read(2) ;
	
		}

	}
	
	
	$VE_Dev_Name_By_IP_HashRef = { };

	foreach my $IP (@VE_Device_IPs) {
	
		print "\nFound Visible Energy Device at : $IP\n" ;
		my $IPStr = "http://" . $IP . "/status.js" ;
		my $DevStatusJSON ;
		my $GetResponse2 ;
		do {
			$GetResponse2 = $UA->get ($IPStr) ;
		} while !($GetResponse2->is_success);
		$DevStatusJSON = $GetResponse2->decoded_content ;
		my $JSONText = $JSON->decode ($DevStatusJSON) ;
		my $DropDownText = $JSONText->{strip}->{identity}->{model} ;
		$DropDownText .= " : " . $JSONText->{strip}->{identity}->{name} ;
		print "\n $DropDownText \n" ;

		$VE_Dev_Name_By_IP_HashRef->{$IP} = $DropDownText ;
		$IP_Corresponding_To_Dev_Name_HashRef->{$DropDownText} = $IP ;
	
	}
	
	@VE_Dev_List = ();
	@IP_List = ();
	$VE_Dev_NA = 1 ;
	foreach my $DevIP (keys %$VE_Dev_Name_By_IP_HashRef) {
		push (@VE_Dev_List, $VE_Dev_Name_By_IP_HashRef->{$DevIP} . " @ " . $DevIP);
		push (@IP_List, $DevIP) ;
		$VE_Dev_NA = 0 ;
	}


}

sub MeasurePower {

	my $SocketNumber = shift ;
	# Expects @LogPower, @SocketsUpdatedName, $IPStr to be shared

	threads->detach();

	my $LoggingInProgress = 0;
	my $CSV_FileName ;
	my $CSV_FileHandle ;

	my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
	my @weekDays = qw(Sun Mon Tue Wed Thu Fri Sat Sun);

	my $CurrNumSamples = 0;
	my $CurrTotalPower = 0;
	my @CurrTotalPower_PS ;
	my $CSVFileStr = "" ;
	my $SampleStartTime ;

	while (1) {
		
		my $SampleStartTime = [gettimeofday] ;
		if ($LogPower[$SocketNumber] == 0) {

			WrapUpInTimeout:
			if ($LoggingInProgress == 1 && $WriteToCSVFile == 1) {
				open ($CSV_FileHandle, ">$CSV_FileName") or 
					die "Can't open $CSV_FileName for writing\nRaw CSV Data: $CSVFileStr" ;
				printf $CSV_FileHandle "$CSVFileStr" ;
				close ($CSV_FileHandle) ;
			}
			$LoggingInProgress = 0 ;
			$CurrNumSamples = 0 ;
			$CurrTotalPower = 0 ;
			@CurrTotalPower_PS = (0, 0, 0, 0) ;
			# Close the CSV file which was open
			sleep 1 ;

			threads->self()->suspend();

		} else {

			if ($LoggingInProgress == 0) {
				my ($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime();
				my $year = 1900 + $yearOffset;
				my $theTime = "$hour-$minute-$second-$weekDays[$dayOfWeek]-$months[$month]-$dayOfMonth-$year" ;
				# Open the file
				my $SocketString = ($SocketNumber < 4) ? sprintf "Socket-%d-%s", $SocketNumber, $SocketsUpdatedName[$SocketNumber] : "All-Sockets" ;
				$CSV_FileName = sprintf "%s-%s.csv", $SocketString, $theTime ;
				print "\n\nStarted recording instantaneous power data...\n" ;
				print "File: $CSV_FileName\n\n" if ($WriteToCSVFile == 1) ;
				if ($SocketNumber != 4) {
					$CSVFileStr = "Time,Instantaneous Power\n" ;
				} else {
					$CSVFileStr = sprintf "Time, Instantaneous Power %s, Instantaneous Power %s, Instantaneous Power %s, Instaneous Power %s\n",
						$SocketsUpdatedName[0], $SocketsUpdatedName[1],	$SocketsUpdatedName[2],	$SocketsUpdatedName[3];
				}
				$LoggingInProgress = 1 ;
			}
			my $CurrPage = ($SocketNumber != 4) ? $IPStr . $SocketNumber . "/watts.js" : $IPStr . "/watts.js?detail" ;
			# printf "\nPolling device for power data at : @$SampleStartTime\n" ;
			my $DevStatusJSON ;
			my $GetResponse3 ;
			my $NumTimeouts = 0 ;
			do {
				do {
					$GetResponse3 = $UA->get ($CurrPage) ;
					$NumTimeouts ++ ;
					if ($NumTimeouts > 10) {
						if ($LogPower[$SocketNumber] == 0) {
							goto WrapUpInTimeout ;
						}
					}
				} while !($GetResponse3->is_success) ;
				$DevStatusJSON = $GetResponse3->decoded_content ;
			} while !(valid_json ($DevStatusJSON)) ;
			my $JSONText = $JSON->decode ($DevStatusJSON) ;
			my $SocketInfoArrayRef = $JSONText->{strip}->{socket} ;
			if ($SocketNumber != 4) {
				my $CurrPower = $SocketInfoArrayRef->[0]->{watts};
				$CurrPower =~ s/[^\d.]//g;
				$CurrTotalPower += $CurrPower ;
				$CurrNumSamples ++ ;
				my $AvPower = 1.0 * $CurrTotalPower / $CurrNumSamples ;
				$CSVFileStr .= sprintf "%d, %.02f\n", $CurrNumSamples, $CurrPower ;
				printf "\r%04d: Current Power: %.02f, Average Power: %.02f                ", 
					$CurrNumSamples, $CurrPower, $AvPower ; # Remeber \r at the beginning
				$| = 1;
				my $CurrTime = [gettimeofday] ;
				my $Elapsed = tv_interval ( $SampleStartTime, $CurrTime );
				my $NumWholeSeconds = int ($Elapsed);
				my $FictRec = 0;
				# printf "\nElapsed Interval : %.02f, NumWholeSeconds : %0d\n", $Elapsed, $NumWholeSeconds ;
				while ($FictRec < $NumWholeSeconds) {
					$CurrNumSamples ++ ;
					$CurrTotalPower += $CurrPower ;
					if ($Debug) {
						$CSVFileStr .= sprintf "%d, %.02f : REPEATED SAMPLE\n", $CurrNumSamples, $CurrPower ;
					} else {
						$CSVFileStr .= sprintf "%d, %.02f\n", $CurrNumSamples, $CurrPower ;
					}
					printf "\r%04d: Current Power: %.02f, Average Power: %.02f : RS %0d of %0d", 
						$CurrNumSamples, $CurrPower, $AvPower, $FictRec + 1, $NumWholeSeconds ;
						# Remeber \r at the beginning
					$| = 1;
					$FictRec ++ ;
				}
				my $SleepInterval = 1.00 - ($Elapsed - $NumWholeSeconds) ;
				# printf "\nSleeping for : %.02f\n", $SleepInterval ;
				Time::HiRes::sleep ($SleepInterval) ;
			} else {
				my @AvPower_PS = (0,0,0,0) ;
				my @CurrPower_PS = (0,0,0,0) ;
				for (my $TmpSocketNum = 0; $TmpSocketNum < 4; $TmpSocketNum ++) {

					my $CurrPower = $SocketInfoArrayRef->[$TmpSocketNum]->{watts};
					$CurrPower =~ s/[^\d.]//g;
					$CurrPower_PS[$TmpSocketNum] = $CurrPower ;
					$CurrTotalPower_PS[$TmpSocketNum] += $CurrPower ;
					if ($TmpSocketNum == 0) {
						$CurrNumSamples ++ ;
					}
					$AvPower_PS[$TmpSocketNum] = 1.0 * $CurrTotalPower_PS[$TmpSocketNum] / $CurrNumSamples ;
					
					if ($TmpSocketNum == 0) {
						$CSVFileStr .= sprintf "%d, %.02f", $CurrNumSamples, $CurrPower ;
					} elsif ($TmpSocketNum < 3) {
						$CSVFileStr .= sprintf ", %.02f", $CurrPower ;
					} else {
						$CSVFileStr .= sprintf ", %.02f\n", $CurrPower ;
					}

				}
				printf "\r%04d: Current Power: ( %.02f, %.02f, %.02f, %.02f ), Average Power: ( %.02f, %.02f, %.02f, %.02f )                ", 
					$CurrNumSamples, 
					$CurrPower_PS[0], $CurrPower_PS[1], $CurrPower_PS[2], $CurrPower_PS[3], 
					$AvPower_PS[0], $AvPower_PS[1], $AvPower_PS[2], $AvPower_PS[3] ; # Remeber \r at the beginning
				$| = 1;
				my $CurrTime = [gettimeofday] ;
				my $Elapsed = tv_interval ( $SampleStartTime, $CurrTime );
				my $NumWholeSeconds = int ($Elapsed);
				my $FictRec = 0;
				# printf "\nElapsed Interval : %.02f, NumWholeSeconds : %0d\n", $Elapsed, $NumWholeSeconds ;
				while ($FictRec < $NumWholeSeconds) {
					$CurrNumSamples ++ ;
					for (my $TmpSocketNum = 0; $TmpSocketNum < 4 ; $TmpSocketNum ++) {
						$CurrTotalPower_PS[$TmpSocketNum] += $CurrPower_PS[$TmpSocketNum] ;
						my $DebugStr = ($Debug) ? " : REPEATED SAMPLE" : "" ;
						if ($TmpSocketNum == 0) {
							$CSVFileStr .= sprintf "%d, %.02f", $CurrNumSamples, $CurrPower_PS[$TmpSocketNum] ;
						} elsif ($TmpSocketNum < 3) {
							$CSVFileStr .= sprintf ", %.02f", $CurrPower_PS[$TmpSocketNum] ;
						} else {
							$CSVFileStr .= sprintf ", %.02f%s\n", $CurrPower_PS[$TmpSocketNum], $DebugStr ;
						}
					}
					$| = 1;
					$FictRec ++ ;
				}
				my $SleepInterval = 1.00 - ($Elapsed - $NumWholeSeconds) ;
				# printf "\nSleeping for : %.02f\n", $SleepInterval ;
				Time::HiRes::sleep ($SleepInterval) ;
			}

		}

	}
		
}

sub ClearScreen {

	system $^O eq 'MSWin32' ? 'cls' : 'clear';

}
